<?php
/**
 *  The Yz_Config class searches the backend (a multidimensional array, ArrayAccess or similar)
 *      for a key specified in dot notation, where a dot separates the corresponding array's keys
 *
 *  Example:
 *      $config['my.test.example'] can be represented as
 *      $config['my']['test']['example']
 *
 *  Changelog:
 *  20120207:   adapt the class for possible multidimensional array values
 *              remove load methods
 *              correct exceptions
 */
class Yz_Config
    extends Yz_Hash
    implements Yz_Config_Interface
{

    /**
     *  The config source title, for use in logs, exceptions etc
     *  The default value is 'Unidentified'
     *  @var string
     */
    protected $source = 'Unidentified';



    /**
     *   the constructor extends the Yz_Hash constructor
     *  @param mixed $values
     *  @param string $source
     *  @throw InvalidArgument if $source is neither null nor string
     */
    public function __construct( $values=null, $source=null )
    {
        parent::__construct( $values );
        if( null !== $source ) {
            $this->source = Yz_String::test( $source );
        }
    } // __construct



    /**
     *  fetch() : get a config value by key
     *  @param  string key
     *  @return mixed|null
     *  @throw InvalidArgument if $key is not a string
     */
    public function fetch( $key )
    {
        $key = Yz_String::testNotEmpty( $key );

        return $this->doFetch( $key, $this->value );
    } // fetch



    /**
     *  fetchString() : get a string value
     *  @param  string key
     *  @return string|null
     *  @throw  Yz_Config_WrongValueTypeException if the found value is not a string
     *  @throw  InvalidArgument if $key is not a string
     */
    public function fetchString( $key, $default='' )
    {
        $value = $this->fetch( $key );

        if ( null === $value ) {

            // ensure the default is also a string
            $value = Yz_String::test( $default );
        } else  {
            $value = Yz_String::valueOf($value);
            if ( null === $value ) {
                $ex = new Yz_Config_WrongValueTypeException();
                throw $ex->withArgs(array(
                    'key' => $key,
                    'src' => $this->source,
                    'expect' => 'string',
                    ));
            }
        }
        return $value;
    } // fetchString



    /**
     *  fetchArray() : get an array value
     *  @param  string $key
     *  @return Yz_Array|null
     *  @throw  Yz_Config_WrongValueTypeException if the found value is not a Yz_Array
     *  @throw  InvalidArgument if $key is not a string
     */
    public function fetchArray( $key, $default=null )
    {
        $value = $this->fetch( $key );

        if ( null === $value ) {

            // ensure the default is also boolean
            $value = new Yz_Array($default);
        } else  {

            // ensure returning a copy
            $v = Yz_Array::valueOf($value);
            if ( null === $value ) {
                $ex = new Yz_Config_WrongValueTypeException();
                throw $ex->withArgs(array(
                    'key' => $key,
                    'src' => $this->source,
                    'expect' => 'array',
                    ));
            }
            $value = new Yz_Array($v);
        }
        return $value;
    } // fetchArray


    /**
     *  fetchBoolean() : get a boolean value
     *  @param  string $key
     *  @return bool|null
     *  @throw  Yz_Config_WrongValueTypeException if the found value is not a boolean
     *  @throw  InvalidArgument if $key is not a string
     */
    public function fetchBoolean( $key, $default=false )
    {
        $value = $this->fetch( $key );

        if ( null === $value ) {

            // ensure the default is also boolean
            $value = is_bool($default) ? $default : null;
        } else  {
            if ( ! is_bool( $value ) ) {
                $ex = new Yz_Config_WrongValueTypeException();
                throw $ex->withArgs(array(
                    'key' => $key,
                    'src' => $this->source,
                    'expect' => 'boolean',
                    ));
            }
        }
        return $value;
    } // fetchBoolean



    /** fetch a config section as a Yz_Config object
     *  @param  string $key
     *  @return Yz_Config|null
     *  @throw  Yz_Config_WrongValueTypeException if the found value is not an array type
     *  @throw  InvalidArgument if $key is not a string
     */
    public function fetchSection( $key )
    {
        debug();

        $value = $this->fetch( $key );

        if ( null !== $value ) {
            try {
                $value = new static($value, $this->source);
            } catch (Yz_Exception $ex) {
                $ex = new Yz_Config_WrongValueTypeException();
                throw $ex->withArgs(array(
                    'key' => $key,
                    'src' => $this->source,
                    'expect' => 'array',
                    ));
            }
        }
        return $value;
    } // fetchSection


    /** fetch a config section as a Yz_Config object
     *  @param  string $key
     *  @return Yz_Config
     *  @throw  Yz_Config_WrongValueTypeException if the found value is not an array type
     *  @throw  InvalidArgument if $key is not a string
     */
    public function fetchSectionWithInherit( $key )
    {
        debug($key);

        $value = $this->fetchSection( $key );

        if ( null === $value ) {
            $value = new static();
        } else {
            $inherit = $value->fetchString("inherit");
            if ($inherit) {
                $value->offsetUnset("inherit");
                $parent = $this->fetchSectionWithInherit($inherit);
                if (! $parent->isEmpty()) {
                    $value = $value->union($parent);
                }
            }
        }
        return $value;
    } // fetchSectionWithInherit


    /** doFetch() : recursively fetch a value by the key
     *  A null return value typically means that no value has been found,
     *  but it is possible to specify "null" explicitly in the config source
     *  @param string $key
     *  @param array $array
     *  @return mixed
     */
    protected function doFetch( $key, array $array )
    {
        // example: input: ..key1..key2..key3..
        $key = trim( $key, '.' );
        // input: key1..key2..key3
        if ( array_key_exists( $key, $array ) ) {
            // test for exact match
            return $array[$key];
        }
        $keys = explode( '.', $key, 2 ); // ["key1", ".key2..key3"]

        // if the count of $keys were 1, then
        // the key doesn't exist in $array
        // otherwise it would have matched when tested above
        if (count($keys) > 1) {
            list($key0, $key1) = $keys;
            if ( array_key_exists( $key0, $array ) ) {
                $next = Yz_Hash::valueOf($array[$key0]);

                if ( null !== $next ) {
                    return $this->doFetch( $key1, $next ); // ".key2..key3"
                }
            }
        }
        return null;
    } // doFetch


} // class

